package com.spzx.common.security.aspect;

import com.alibaba.fastjson2.JSON;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.common.security.annotation.SpzxCache;
import com.spzx.common.security.config.ThreadPoolUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Aspect
@Component
@Slf4j
public class SpzxCachedAspect {
    @Resource
    RedisTemplate redisTemplate;
    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Resource
    Redisson redisson ;
    @Around(value = "@annotation(spzxCache)")
    public Object around(ProceedingJoinPoint joinPoint, SpzxCache spzxCache) {
        //获取布隆过滤器：判断数据是否存在
        //        1、拼接缓存的key：获取注解的key + 目标方法的参数列表
        String prefix = spzxCache.prefix();
        Object[] args = joinPoint.getArgs();
        //如果参数列表未空，返回none ， 如果不为空遍历参数列表 使用:拼接字符串返回
        //前端提交的参数 后端如果pojo/请求体入参  后端会创建一个对象接收，相同的参数值 后端接收时通过不同对象接收的
        String cacheSuffix = (args == null || args.length == 0) ? ":none" :
                Arrays.asList(args).stream().map(arg -> JSON.toJSONString(arg)).collect(Collectors.joining(":"));
        String cacheKey = prefix + cacheSuffix;
      //获取布隆过滤器
        RBloomFilter<Object> rBloomFilter = redisson.getBloomFilter("spzx:channel:sku:bloom");
        //判断是否存在数据
        if(args != null && args.length > 0 && spzxCache.isBloomValidate()){

            boolean contains = rBloomFilter.contains(args[0]);
            if(!contains){
                log.error("数据不存在......");
              return null;
            };
        }

        //        3、获取分布式锁：拼接分布式锁的key : 缓存的key+ lock锁的标识
        String lockKey = cacheKey + spzxCache.lockSuffix();
        String uuid = UUID.randomUUID().toString().replace("-", "");
        try {
//        2、查询缓存
            Object cache = redisTemplate.opsForValue().get(cacheKey);
            if (cache != null) {
                //命中缓存  返回结果
                return cache;
            }
            //自旋尝试获取锁 //自动续期
            getLock(lockKey,uuid,spzxCache);
            //获取到分布式锁
            //        4、再次查询缓存：
            //        防止先获取到锁的线程执行时，其他线程查询缓存失败 再获取分布式锁的位置自旋
            //        先进入的线程执行完业务后会将查询的数据缓存到redis，为了避免阻塞的线程再次查询数据库
            cache = redisTemplate.opsForValue().get(cacheKey);
            if (cache != null) {
                //命中缓存  返回结果
                return cache;
            }
            //        5、执行目标方法
            Object result = joinPoint.proceed();//
            //        将返回的结果缓存 ，设置过期时间(空值设置较短的过期时间，非空设置较长的过期时间)
            if (result == null && spzxCache.isSaveNull()) {
                redisTemplate.opsForValue().set(cacheKey, result,
                        spzxCache.nullValCachedTime(), TimeUnit.SECONDS);
            } else if (result != null) {
                redisTemplate.opsForValue().set(cacheKey,result,
                        spzxCache.notNullValCachedTime()+ new Random().nextLong(spzxCache.random()), TimeUnit.SECONDS);
            }
            return result;

        } catch (Throwable e) {
//            throw new RuntimeException(e);
            log.error(ExceptionUtils.getStackTrace(e));
        } finally {
          releaseLock(lockKey,uuid);
        }
        //如果上面的代码执行时 出现异常 catch中没有结束业务(return或者抛出异常)，仍然可以继续执行后面的业务
        try {
            //兜底：避免上面出现redis缓存的操作异常  直接查询数据库数据
            return joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    private void releaseLock(String lockKey , String uuid) {
        //        6、释放分布式锁：删除分布式锁(在finally中编写)
        String releaseLuaScript = "if redis.call('exists' , KEYS[1]) == 0 " +//锁不存在
                "  then return 0 " +
                " elseif redis.call('hexists' , KEYS[1] , ARGV[1]) == 0 " + //锁不是当前线程的
                "  then return -1  " +
                " elseif redis.call('hincrby' , KEYS[1] , ARGV[1] , -1) > 0 " + //是自己的锁 释放后 使用锁的次数仍然>0
                "   then  return 2 " +
                " else  redis.call('del' , KEYS[1] ) return 1 " + //释放锁
                " end";
        Long execute = stringRedisTemplate.execute(new DefaultRedisScript<>(releaseLuaScript, Long.class),
                Arrays.asList(lockKey), uuid);

        System.out.println(execute);
        //        7、对上面的代码进行try catch 如果出现异常 直接执行目标方法 将数据库查询的结果返回 (兜底)
    }
    /*
    if redis.call('exists' , KEYS[1]) == 0  then redis.call('hset' , KEYS[1] ,ARGV[1] , 1 )  redis.call('expire', KEYS[1] , ARGV[2] )      return 1  elseif  redis.call('hexists' , KEYS[1] , ARGV[1]) == 1  then redis.call('hincrby' , KEYS[1] , ARGV[1] ,1 )      return 1  else      return 0  end
     */
    private void getLock(String lockKey , String uuid , SpzxCache spzxCache) {
        String getLockLuaScript = "if redis.call('exists' , KEYS[1]) == 0 " + //锁不存在
                " then redis.call('hset' , KEYS[1] ,ARGV[1] , 1 )  redis.call('expire', KEYS[1] , ARGV[2] ) " +//直接获取锁 返回1
                "     return 1 " +
                " elseif  redis.call('hexists' , KEYS[1] , ARGV[1]) == 1 " + //锁存在 而且是自己的
                " then redis.call('hincrby' , KEYS[1] , ARGV[1] ,1 ) " + //获取锁成功 获取次数+1
                "     return 1 " +
                " else " +  //锁存在 不是自己的 获取失败
                "     return 0 " +
                " end";
        System.out.println(getLockLuaScript);
        Boolean flag = stringRedisTemplate.execute(new DefaultRedisScript<>(getLockLuaScript, Boolean.class),
                Arrays.asList(lockKey), uuid, spzxCache.lockExpire());
        if(flag){
            //获取锁成功
            //启动自动续期的线程
            renewExpire(lockKey ,spzxCache.lockExpire() ,TimeUnit.SECONDS , uuid );
        }else{
            //获取锁失败，自旋尝试再次获取
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            getLock(lockKey,uuid,spzxCache);
        }
    }

    private void renewExpire(String key, String expire ,TimeUnit unit , String uuid ) {
        ThreadPoolUtil.threadPoolExecutor().execute(()->{
            try {
                Thread.sleep(Long.parseLong(expire)*2000/3 );
                //当键存在  休眠过期时间的2/3时长后 再更新过期时间
                System.out.println(stringRedisTemplate.opsForHash().hasKey(key, uuid));
                System.out.println(stringRedisTemplate.opsForHash().get(key, uuid));
                while (stringRedisTemplate.opsForHash().hasKey(key , uuid)) {//判断hash结构是否存在uuid属性
                    //给键设置过期时间
                    stringRedisTemplate.expire(key, Long.parseLong(expire), unit);
                    Thread.sleep(Long.parseLong(expire)*2000/3 );
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
    }

}
